/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

namespace Xceed.Silverlight.Data
{
  internal sealed class DataRange
  {
    private DataRange(
      IDictionary<string, object> rangeStart,
      SourcePosition rangeStartPosition,
      bool startInclusive,
      IDictionary<string, object> rangeEnd,
      SourcePosition rangeEndPosition,
      bool endInclusive,
      DataRangeContext context,
      bool isSelectionRangeInclusive )
    {
      if( rangeStartPosition == null )
        throw new ArgumentNullException( "rangeStartPosition" );

      if( rangeEndPosition == null )
        throw new ArgumentNullException( "rangeEndPosition" );

      if( rangeStart == null && !rangeStartPosition.IsOutOfBound )
        throw new ArgumentException( "rangeStart cannot be null when the rangeStartPosition is within bounds.", "rangeStart" );

      if( rangeEnd == null && !rangeEndPosition.IsOutOfBound )
        throw new ArgumentException( "rangeEnd cannot be null when the rangeEndPosition is within bounds.", "rangeEnd" );

      if( context == null )
        throw new ArgumentNullException( "context" );

      m_startRange = rangeStart;
      m_startRangePosition = rangeStartPosition;
      m_isStartInclusive = startInclusive;
      m_endRange = rangeEnd;
      m_endRangePosition = rangeEndPosition;
      m_isEndInclusive = endInclusive;
      m_context = context;
      m_isSelectionRangeInclusive = isSelectionRangeInclusive;
      AffectedSortDirection = GetSortAffectingDataRangeDirection();

      this.CheckDataRangeConsistency();
      this.MakeStartRangeLowest();
    }


    #region StartPosition Property (Internal)

    internal SourcePosition StartPosition
    {
      get
      {
        return m_startRangePosition;
      }
    }

    private SourcePosition m_startRangePosition; //null

    #endregion StartPosition Property (Internal)

    #region EndPosition Property (Internal)

    internal SourcePosition EndPosition
    {
      get
      {
        return m_endRangePosition;
      }
    }

    private SourcePosition m_endRangePosition; //null

    #endregion EndPosition Property (Internal)

    #region Context Property (Internal)

    internal DataRangeContext Context
    {
      get
      {
        return m_context;
      }
    }

    private DataRangeContext m_context; //null

    #endregion Context Property (Internal)

    #region IsSelectionRangeInclusive Property (Internal)

    internal bool IsSelectionRangeInclusive
    {
      get
      {
        return m_isSelectionRangeInclusive;
      }
    }

    private bool m_isSelectionRangeInclusive; //false

    #endregion IsSelectionRangeInclusive Property (Internal)

    #region AffectedSortDirection Property (Internal)

    internal ListSortDirection AffectedSortDirection
    {
      get;
      private set;
    }

    #endregion AffectedSortDirection Property (Internal)


    private static IDictionary<string, object> ExtractValues(
      RawDataItem dataItem,
      IEnumerable<SortDescription> sortDescriptions )
    {
      Dictionary<string, object> keyValues = new Dictionary<string, object>();

      foreach( SortDescription sortDescription in sortDescriptions )
      {
        string memberName = sortDescription.PropertyName;
        object value = dataItem.GetDataMemberValue( memberName );

        keyValues.Add( memberName, value );
      }

      return keyValues;
    }

    private static SortPosition CreateSortPosition(
      IDictionary<string, object> keyValues,
      IList<SortDescription> sortDescriptions,
      bool startBefore )
    {
      if( keyValues == null || sortDescriptions == null )
        return null;

      IList<object> values = new List<object>();

      foreach( SortDescription sort in sortDescriptions )
      {
        object value;

        if( keyValues.TryGetValue( sort.PropertyName, out value ) )
        {
          values.Add( value );
        }        
      }

      StartPosition startPosition = ( startBefore )
                                      ? Xceed.Silverlight.Data.StartPosition.Before
                                      : Xceed.Silverlight.Data.StartPosition.After;

      return new SortPosition(
                   sortDescriptions,
                   values,
                   startPosition );
    }

    private static ItemPosition CreateItemPosition( DataPath dataPath )
    {
      if( dataPath == null )
        return null;

      RawDataItem dataItem = dataPath.LastChild as RawDataItem;

      if( dataItem == null )
        return null;

      return new ItemPosition( dataItem );
    }

    private static Func<string, object> GetMemberValue( RawDataItem dataItem )
    {
      return dataItem.GetDataMemberValue;
    }

    private static int CompareValueHelper( object a, object b )
    {
      if( a == b )
        return 0;

      if( a == null )
        return -1;

      if( b == null )
        return 1;

      IComparable comparable = a as IComparable;

      if( comparable == null )
        throw new ArgumentException( "The specified objects must implement IComparable." );

      return comparable.CompareTo( b );
    }

    private static int CompareHelper(
      RawDataItem dataItem,
      IDictionary<string, object> keyValues,
      bool isRangeInclusive,
      IEnumerable<SortDescription> sortDescriptions )
    {
      int compareResult = 0;

      foreach( SortDescription sortDesc in sortDescriptions )
      {
        string propName = sortDesc.PropertyName;
        object firstValue;
        object secondValue;
        bool isFirstValueFound = dataItem.TryGetDataMemberValue( propName, out firstValue );
        bool isSecondValueFound = keyValues.TryGetValue( propName, out secondValue );

        compareResult = DataRange.CompareValueHelper( isFirstValueFound, firstValue, true, isSecondValueFound, secondValue, isRangeInclusive, sortDesc );
        if( compareResult != 0 )
          return compareResult;  
      }

      return 0;
    }

    internal static int CompareHelper(
      IDictionary<string, object> first,
      bool isFirstRangeInclusive,
      IDictionary<string, object> second,
      bool isSecondRangeInclusive,
      IEnumerable<SortDescription> sortDescriptions )
    {
      int compareResult = 0;

      foreach( SortDescription sortDesc in sortDescriptions )
      {
        string propName = sortDesc.PropertyName;
        object firstValue;
        object secondValue;
        bool isFirstValueFound = first.TryGetValue( propName, out firstValue );
        bool isSecondValueFound = second.TryGetValue( propName, out secondValue ); 

        compareResult = DataRange.CompareValueHelper( isFirstValueFound, firstValue, isFirstRangeInclusive, isSecondValueFound, secondValue, isSecondRangeInclusive, sortDesc );
        if( compareResult != 0 )
          return compareResult;       
      }

      return 0;
    }

    private static int CompareValueHelper( bool isFirstValueFound, object firstValue, bool isFirstRangeInclusive, 
                                          bool isSecondValueFound, object secondValue,  bool isSecondRangeInclusive, SortDescription sortDesc )
    {
      if( isFirstValueFound && isSecondValueFound )
      {
        int compare = DataRange.CompareValueHelper( firstValue, secondValue );

        if( compare < 0 )
          return ( sortDesc.Direction == ListSortDirection.Ascending ) ? -1 : 1;
        else if( compare > 0 )
          return ( sortDesc.Direction == ListSortDirection.Ascending ) ? 1 : -1;

        return 0;
      }
      else if( (!isFirstValueFound && isSecondValueFound)
            || ( isFirstValueFound && !isSecondValueFound ) )
      {
        return ( sortDesc.Direction == ListSortDirection.Ascending ) ? -1 : 1;      
      }      

      return 0;
    }

    private ListSortDirection GetSortAffectingDataRangeDirection()
    {
      foreach( SortDescription sortDesc in m_context.SortDescriptions )
      {
        string propName = sortDesc.PropertyName;
        object firstValue = null;
        object secondValue = null;
        bool isFirstValueFound = false;
        bool isSecondValueFound = false;

        if( m_startRange != null )
        {
          isFirstValueFound = m_startRange.TryGetValue( propName, out firstValue ); 
        }
        if( m_endRange != null )
        {
          isSecondValueFound = m_endRange.TryGetValue( propName, out secondValue ); 
        }

        bool isSameValues = false;

        if( isFirstValueFound && isSecondValueFound )
          isSameValues = ( firstValue == secondValue );
        else if( !isFirstValueFound && !isSecondValueFound )
          isSameValues = true;

        if( !isSameValues )
          return sortDesc.Direction;
      }

      return ListSortDirection.Ascending;
    }

    private void SwapBoundaries()
    {
      IDictionary<string, object> swapKeyValues = m_endRange;
      SourcePosition swapSourcePosition = m_endRangePosition;
      bool swapInclusive = m_isEndInclusive;

      m_endRange = m_startRange;
      m_endRangePosition = m_startRangePosition;
      m_isEndInclusive = m_isStartInclusive;

      m_startRange = swapKeyValues;
      m_startRangePosition = swapSourcePosition;
      m_isStartInclusive = swapInclusive;

      // The StartPosition flag within the SortPosition
      // must be inversed to keep the same range.
      if( m_startRangePosition is SortPosition )
      {
        m_startRangePosition = DataRange.CreateSortPosition(
                                 m_startRange,
                                 m_context.SortDescriptions,
                                 m_isStartInclusive );
      }

      if( m_endRangePosition is SortPosition )
      {
        m_endRangePosition = DataRange.CreateSortPosition(
                               m_endRange,
                               m_context.SortDescriptions,
                               !m_isEndInclusive );
      }
    }

    private void CheckDataRangeConsistency()
    {
      ValidateRangeInfoSorts( m_startRange );
      ValidateRangeInfoSorts( m_endRange );

      ValidateRangeInfoPreviousSorts( m_startRange );
      ValidateRangeInfoPreviousSorts( m_endRange );      
    }

    private void ValidateRangeInfoSorts( IDictionary<string, object> rangeInfo )
    {
      IList<SortDescription> sorts = m_context.SortDescriptions;

      // Check if all sort criteria of the StartRangeInfo/EndRangeInfo are contained in the Range's sort descriptions
      if( rangeInfo != null )
      {
        for( int i = 0; i < rangeInfo.Count; ++i )
        {
          bool isKeyInRange = sorts.Any( ( SortDescription ) => SortDescription.PropertyName == rangeInfo.ElementAt( i ).Key );
          if( !isKeyInRange )
          {
            throw new ArgumentException( "The specified range info contains the value '" + rangeInfo.ElementAt( i ).Key + "', which is not a valid range sort.", "rangeInfo" );
          }
        }
      }
    }

    private void ValidateRangeInfoPreviousSorts( IDictionary<string, object> rangeInfo )
    {
      IList<SortDescription> sorts = m_context.SortDescriptions;

      // Check if a Range sort is in the StartRangeInfo/EndRangeInfo, if yes, make sure all previous range sorts are included.
      if( rangeInfo != null )
      {
        object outValue = null;

        for( int i = sorts.Count - 1; i >= 0; --i )
        {
          if( rangeInfo.TryGetValue( sorts[ i ].PropertyName, out outValue ) )
          {
            for( int j = 0; j <= i - 1; ++j )
            {
              if( !rangeInfo.TryGetValue( sorts[ j ].PropertyName, out outValue ) )
              {
                throw new ArgumentException( "The specified range info is missing missing the sort '" + sorts[ j ].PropertyName + "' primary sort.", "rangeInfo" );
              }
            }
            return;
          }
        }
      }
    }

    internal bool IsSameRange( DataRange obj )
    {
      SortPosition currentRangePos = this.StartPosition as SortPosition;
      SortPosition objRangePos = obj.StartPosition as SortPosition;

      if( ( ( currentRangePos == null ) && ( objRangePos != null ) )
       || ( ( currentRangePos != null ) && ( objRangePos == null ) ) )
        return false;

      if( !currentRangePos.SortingReference.SequenceEqual( objRangePos.SortingReference ) )
        return false;
      if( !currentRangePos.SortValues.SequenceEqual( objRangePos.SortValues ) )
        return false;
      if( currentRangePos.StartPosition != objRangePos.StartPosition )
        return false;

      currentRangePos = this.EndPosition as SortPosition;
      objRangePos = obj.EndPosition as SortPosition;
      if( !currentRangePos.SortingReference.SequenceEqual( objRangePos.SortingReference ) )
        return false;
      if( !currentRangePos.SortValues.SequenceEqual( objRangePos.SortValues ) )
        return false;
      if( currentRangePos.StartPosition != objRangePos.StartPosition )
        return false;

      if( !this.Context.Equals( obj.Context ) )
        return false;

      return true;
    }

    private void MakeStartRangeLowest()
    {
      // Make sure the range start is lower or equal than the range end.
      bool isStartRangeOutOfBound = m_startRangePosition.IsOutOfBound;
      bool isEndRangeOutOfBound = m_endRangePosition.IsOutOfBound;
      IList<SortDescription> sorts = m_context.SortDescriptions;     
      bool swap = false;
      bool isAscending = AffectedSortDirection == ListSortDirection.Ascending;

      if( isStartRangeOutOfBound && isEndRangeOutOfBound )
      {
        if( m_startRangePosition != m_endRangePosition )
        {
          swap = isAscending && m_startRangePosition.IsAfterLast
             || !isAscending && m_startRangePosition.IsBeforeFirst;
        }
      }
      else if( !isStartRangeOutOfBound && !isEndRangeOutOfBound )
      {
        int compare = DataRange.CompareHelper( m_startRange, m_isStartInclusive, m_endRange, m_isEndInclusive, sorts );

        swap = isAscending && compare > 0
           || !isAscending && compare < 0;
      }
      else if( isStartRangeOutOfBound )
      {
        swap = isAscending && m_startRangePosition.IsAfterLast
           || !isAscending && m_startRangePosition.IsBeforeFirst;
      }
      else if( isEndRangeOutOfBound )
      {
        swap = isAscending && m_endRangePosition.IsBeforeFirst
           || !isAscending && m_endRangePosition.IsAfterLast;
      }

      if( swap )
      {
        this.SwapBoundaries();
      }
    }

    internal bool Contains( DataPath item )
    {
      // A DataPath is contained in the range only if it is within the boundaries
      // and it isn't filtered out by a local or remote filter.
      return !this.IsOutside( item )
          && !this.IsFilteredOut( item );
    }

    internal bool IsOutside( DataPath item )
    {
      bool isOutside = true;

      // Only user objects are allowed in the range.
      if( item != null && item.LastChild is RawDataItem )
      {
        isOutside = false;
        RawDataItem dataItem = ( RawDataItem )item.LastChild;
        IEnumerable<SortDescription> sorts = m_context.SortDescriptions;

        if( !isOutside && m_startRange != null )
        {
          int compare = DataRange.CompareHelper( dataItem, m_startRange, m_isStartInclusive, sorts );

          isOutside = ( compare < 0 || ( compare == 0 && !m_isStartInclusive ) );
        }

        if( !isOutside && m_endRange != null )
        {
          int compare = DataRange.CompareHelper( dataItem, m_endRange, m_isEndInclusive, sorts );

          isOutside = ( compare > 0 || ( compare == 0 && !m_isEndInclusive ) );
        }
      }

      return isOutside;
    }

    internal bool IsFilteredOut( DataPath item )
    {
      bool isFilteredOut = true;

      // Only user objects are allowed in the range.
      if( item != null && item.LastChild is RawDataItem )
      {
        isFilteredOut = false;
        RawDataItem dataItem = ( RawDataItem )item.LastChild;

        if( m_filterPredicate == null )
        {
          Predicate<object> localFilter = m_context.Filter;
          Func<object, bool> remoteFilter = null;

          if( m_context.FilterExpression != null )
          {
            PredicateBuilder pb = new PredicateBuilder( m_context.FilterExpression );

            remoteFilter = pb.ToPredicate<object>( m_context.UserObjectType, null );
          }

          if( localFilter != null || remoteFilter != null )
          {
            m_filterPredicate =
              ( object value ) =>
              {
                if( localFilter != null )
                {
                  if( !localFilter.Invoke( ( ( RawDataItem )value ).Data ) )
                    return false;
                }

                if( remoteFilter != null )
                {
                  if( !remoteFilter.Invoke( value ) )
                    return false;
                }

                return true;
              };
          }
        }

        if( m_filterPredicate != null )
        {
          isFilteredOut = !m_filterPredicate.Invoke( dataItem );
        }
      }

      return isFilteredOut;
    }

    internal static DataRange All( bool isSelectionRangeInclusive, DataRangeContext context )
    {
      return new DataRange(
                   null,
                   SourcePosition.BeforeFirst,
                   false,
                   null,
                   SourcePosition.AfterLast,
                   false,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From( 
      bool isSelectionRangeInclusive,
      IDictionary<string, object> rangeStart, 
      bool startInclusive, 
      DataRangeContext context )
    {
      SortPosition position = null;

      if( context != null )
      {
        position = DataRange.CreateSortPosition(
                     rangeStart,
                     context.SortDescriptions,
                     startInclusive );
      }

      return new DataRange(
                   rangeStart,
                   position,
                   startInclusive,
                   null,
                   SourcePosition.AfterLast,
                   false,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      DataPath rangeStart,
      bool startInclusive,
      DataRangeContext context )
    {
      IDictionary<string, object> keyValues = null;
      ItemPosition position = DataRange.CreateItemPosition( rangeStart );

      if( position != null && context != null )
      {
        keyValues = DataRange.ExtractValues( 
                      position.Item as RawDataItem, 
                      context.SortDescriptions );
      }

      return new DataRange(
                   keyValues,
                   position,
                   startInclusive,
                   null,
                   SourcePosition.AfterLast,
                   false,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      IDictionary<string, object> rangeStart,
      bool startInclusive,
      IDictionary<string, object> rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      SortPosition rangeStartPosition = null;
      SortPosition rangeEndPosition = null;

      if( context != null )
      {
        IList<SortDescription> sorts = context.SortDescriptions;

        rangeStartPosition = DataRange.CreateSortPosition(
                               rangeStart,
                               sorts,
                               startInclusive );
        rangeEndPosition = DataRange.CreateSortPosition(
                             rangeEnd,
                             sorts,
                             !endInclusive );
      }

      return new DataRange(
                   rangeStart,
                   rangeStartPosition,
                   startInclusive,
                   rangeEnd,
                   rangeEndPosition,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      DataPath rangeStart,
      bool startInclusive,
      IDictionary<string, object> rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      IDictionary<string, object> keyValues = null;
      ItemPosition rangeStartPosition = DataRange.CreateItemPosition( rangeStart );
      SortPosition rangeEndPosition = null;

      if( context != null )
      {
        IList<SortDescription> sorts = context.SortDescriptions;

        if( rangeStartPosition != null )
        {
          keyValues = DataRange.ExtractValues(
                        rangeStartPosition.Item as RawDataItem,
                        context.SortDescriptions );
        }

        rangeEndPosition = DataRange.CreateSortPosition(
                             rangeEnd,
                             sorts,
                             !endInclusive );
      }

      return new DataRange(
                   keyValues,
                   rangeStartPosition,
                   startInclusive,
                   rangeEnd,
                   rangeEndPosition,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      IDictionary<string, object> rangeStart,
      bool startInclusive,
      DataPath rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      IDictionary<string, object> keyValues = null;
      SortPosition rangeStartPosition = null;
      ItemPosition rangeEndPosition = DataRange.CreateItemPosition( rangeEnd );

      if( context != null )
      {
        IList<SortDescription> sorts = context.SortDescriptions;

        rangeStartPosition = DataRange.CreateSortPosition(
                               rangeStart,
                               sorts,
                               startInclusive );

        if( rangeEndPosition != null )
        {
          keyValues = DataRange.ExtractValues(
                        rangeEndPosition.Item as RawDataItem,
                        sorts );
        }
      }

      return new DataRange(
                   rangeStart,
                   rangeStartPosition,
                   startInclusive,
                   keyValues,
                   rangeEndPosition,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange From(
      bool isSelectionRangeInclusive,
      DataPath rangeStart,
      bool startInclusive,
      DataPath rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      IDictionary<string, object> rangeStartValues = null;
      IDictionary<string, object> rangeEndValues = null;
      ItemPosition rangeStartPosition = DataRange.CreateItemPosition( rangeStart );
      ItemPosition rangeEndPosition = DataRange.CreateItemPosition( rangeEnd );

      if( context != null )
      {
        IList<SortDescription> sorts = context.SortDescriptions;

        if( rangeStartPosition != null )
        {
          rangeStartValues = DataRange.ExtractValues(
                               rangeStartPosition.Item as RawDataItem,
                               sorts );
        }

        if( rangeEndPosition != null )
        {
          rangeEndValues = DataRange.ExtractValues(
                             rangeEndPosition.Item as RawDataItem,
                             sorts );
        }
      }

      return new DataRange(
                   rangeStartValues,
                   rangeStartPosition,
                   startInclusive,
                   rangeEndValues,
                   rangeEndPosition,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange To(
      bool isSelectionRangeInclusive,
      IDictionary<string, object> rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      SortPosition position = null;

      if( context != null )
      {
        position = DataRange.CreateSortPosition(
                     rangeEnd,
                     context.SortDescriptions,
                     !endInclusive );
      }

      return new DataRange(
                   null,
                   SourcePosition.BeforeFirst,
                   false,
                   rangeEnd,
                   position,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }

    internal static DataRange To(
      bool isSelectionRangeInclusive,
      DataPath rangeEnd,
      bool endInclusive,
      DataRangeContext context )
    {
      IDictionary<string, object> keyValues = null;
      ItemPosition position = DataRange.CreateItemPosition( rangeEnd );

      if( position != null && context != null )
      {
        keyValues = DataRange.ExtractValues(
                      position.Item as RawDataItem,
                      context.SortDescriptions );
      }

      return new DataRange(
                   null,
                   SourcePosition.BeforeFirst,
                   false,
                   keyValues,
                   position,
                   endInclusive,
                   context,
                   isSelectionRangeInclusive );
    }


    #region Private Fields

    private IDictionary<string, object> m_startRange; //null
    private bool m_isStartInclusive; //false
    private IDictionary<string, object> m_endRange; //null
    private bool m_isEndInclusive; //false
    private Func<object, bool> m_filterPredicate; //null

    #endregion Private Fields
  }
}
